require 'socket'
require 'thread'
require_relative 'connection'
require_relative 'connection_error'
require_relative 'connection_closed_error'
require_relative '../pdu/pdu_factory'
require_relative '../pdu/pdu'
require_relative '../util/byte_buffer'

module SmppClient

  module Net

    # SMPP connection over TCP.

    class TcpConnection < Connection

      include SmppClient::Util
      include SmppClient::Pdu

      # SMSC host and port.
      def initialize(host, port)
        super(host, port)
      end

      # Open connection or raise ConnectionError.
      def open
        begin
          @socket = TCPSocket.new(@host, @port)
          @buffer = ByteBuffer.new
        rescue
          raise ConnectionError, "TCP connection to #{@host}:#{@port} failed.", $@
        end
      end

      # Read incoming bytes and return one full pdu if possible.
      # if connection closed ConnectionClosedError raised.
      # If PDU invalid - PduError.
      def read
        begin
          data_available = select([@socket], nil, nil, 0.2)
        rescue TypeError
          raise ConnectionClosedError # thrown, when @socket is nil
        end
        if data_available
          string = @socket.recv(@max_buffer_size - @buffer.size)
          if string.size > 0
            bytes = string.unpack('c*')
            @buffer.append_bytes(bytes)
            if @buffer.size > ByteBuffer::INT_SZ
              pdu_length = @buffer.read_int
              if pdu_length < Pdu::Pdu::HEADER_LENGTH
                # todo look bottom
                @buffer = ByteBuffer.new
              elsif pdu_length <= @buffer.size
                pdu_bytes = @buffer.remove_bytes(pdu_length)
                pdu = PduFactory.parse(pdu_bytes)
                return pdu
              elsif @buffer.size == @max_buffer_size
                # todo may be just log warning here. Or create WrongPduLengthError class and put in both places.
                raise PduError, "PDU length #{pdu_length} octets, but buffer size #{@max_buffer_size} only"
              end
            end
          end
        end
        return nil
      rescue IOError
        check_io_error
      end

      # Write outgoing PDU. if connection closed ConnectionClosedError raised.
      # If PDU invalid - PduError.
      def write(pdu)
        @socket.write(pdu.buffer.bytes.pack('c*'))
      rescue NoMethodError
        raise ConnectionClosedError # thrown, when @socket is nil
      rescue IOError
        check_io_error
      end

      # Close connection. If connection already closed, method will do nothing.
      # After connection close #read and #write operations will raise ConnectionClosedError.
      # Connection may be reopened by calling #open method.
      def close
        if @socket != nil
          @socket.close
#          @socket = nil
        end
      end

      private

      def check_io_error
        if $!.message == 'closed stream'
          raise ConnectionClosedError
        else
          raise ConnectionError, $!, $@
        end
      end

    end

  end

end